/* NumberType.java
 *
 * Copyright 2006, Tim Dwelle.
 *
 * Licensed under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in
 * compliance with the License.  You may obtain a copy of
 * the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in
 * writing, software distributed under the License is
 * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
 * CONDITIONS OF ANY KIND, either express or implied.  See
 * the License for the specific language governing
 * permissions and limitations under the License.
 *
 */

package dowry.datatype;

import java.math.*;
import java.util.*;
import dowry.*;

/**
 * Object representation of a number datatype.
 *
 */
public class NumberType extends AbstractType
{
    /**
     * Constant for the string literal representing the
     * decimal separator.
     *
     */
    public static final String DECIMAL_SEPARATOR = Messages.get(".");

    /**
     * Constant for the error message when validation finds
     * a value that should be an integer, but contains a
     * decimal point.
     *
     */
    public static final String ERROR_DECIMAL_SEPARATOR_NOT_PERMITTED =
        Messages.get("ERROR_DECIMAL_SEPARATOR_NOT_PERMITTED");

    /**
     * Constant for the error message when validation finds
     * a value that exceeds the maximum value.
     *
     */
    public static final String ERROR_EXCEEDS_MAXIMUM_VALUE =
        Messages.get("ERROR_EXCEEDS_MAXIMUM_VALUE");

    /**
     * Constant for the error message when validation finds
     * a value that exceeds the minimum value.
     *
     */
    public static final String ERROR_EXCEEDS_MINIMUM_VALUE =
        Messages.get("ERROR_EXCEEDS_MINIMUM_VALUE");

    /**
     * Constant for the error message when validation finds
     * a value that exceeds the precision (specifically,
     * when the left-side of the decimal point is too large
     * to allow for the specified scale).
     *
     */
    public static final String ERROR_EXCEEDS_PRECISION =
        Messages.get("ERROR_EXCEEDS_PRECISION");

    /**
     * Constant for the error message when validation finds
     * a value that exceeds the scale.
     *
     */
    public static final String ERROR_EXCEEDS_SCALE =
        Messages.get("ERROR_EXCEEDS_SCALE");

    /**
     * Constant for the error message when validation finds
     * a negative value when the datatype only allows
     * non-negative.
     *
     */
    public static final String ERROR_NEGATIVE_NOT_PERMITTED =
        Messages.get("ERROR_NEGATIVE_NOT_PERMITTED");

    /**
     * Constant for the error message when validation finds
     * a value that does not represent a number.
     *
     */
    public static final String ERROR_NOT_A_NUMBER =
        Messages.get("ERROR_NOT_A_NUMBER");

    /**
     * Constant for the error message when validation finds
     * a positive value when the datatype only allows
     * non-positive.
     *
     */
    public static final String ERROR_POSITIVE_NOT_PERMITTED =
        Messages.get("ERROR_POSITIVE_NOT_PERMITTED");

    /**
     * Constant for the error message when validation finds
     * a value of zero when the datatype is configured as
     * non-zero.
     *
     */
    public static final String ERROR_ZERO_NOT_PERMITTED =
        Messages.get("ERROR_ZERO_NOT_PERMITTED");


    /**
     * Constant for the configuration key containing the
     * datatype's <code>maxValue</code> property indicating
     * the maximum allowable numeric value.
     *
     */
    public static final String MAX_VALUE = "maxValue";

    /**
     * Constant for the configuration key containing the
     * datatype's <code>minValue</code> property indicating
     * the minimum allowable numeric value.
     *
     */
    public static final String MIN_VALUE = "minValue";

    /**
     * Constant for the datatype's name.
     *
     */
    public static final String NAME = Messages.get("number");

    /**
     * Constant for the configuration key containing the
     * datatype's <code>negative</code> property indicating
     * whether or not the number cannot be positive
     * (<code>num <= 0</code>).
     *
     */
    public static final String NEGATIVE = "negative";

    /**
     * Constant for the configuration key containing the
     * datatype's <code>nonZero</code> property indicating
     * whether or not the number cannot be zero (<code>
     * num != 0</code>).
     *
     */
    public static final String NON_ZERO = "nonZero";

    /**
     * Constant for the configuration key containing the
     * datatype's <code>positive</code> property indicating
     * whether or not the number cannot be negative
     * (<code>num >= 0</code>).
     *
     */
    public static final String POSITIVE = "positive";

    /**
     * Constant for the configuration key containing the
     * datatype's <code>precision</code> property indicating
     * the total number of digits in the unscaled value.
     *
     */
    public static final String PRECISION = "precision";

    /**
     * Constant for the configuration key containing the
     * datatype's <code>scale</code> property indicating the
     * number of digits to the right of the decimal point.
     *
     */
    public static final String SCALE = "scale";

    /**
     * Constant for the big decimal value representing 0.
     *
     */
    public static final BigDecimal ZERO = new BigDecimal(0);

    /**
     * Gets the name of the Javascript class that serves
     * as the client side equivalent to this Java Datatype.
     *
     * @return  the string Javascript class name of the
     *          datatype
     *
     */
    public String getJsClass()
    {
        return NumberType.class.getName();
    }

    /**
     * Tests if this datatype is a handler for the
     * specified Dowry type name.
     *
     * @param  typeName  the Dowry type name
     *
     * @return           true if this datatype can handle
     *                   the specified type name; false
     *                   otherwise
     *
     */
    public boolean isType(String typeName)
    {
        return (typeName != null && typeName.equals(NAME));
    }

    /**
     * Converts the Java class for this datatype to the type
     * representation used by Dowry on the client.
     *
     * <p>
     * This implementation returns 'number'.
     * </p>
     *
     * @param  c   the class to get the type name for
     *
     * @return     the string representation of type to be
     *             used in Dowry
     *
     */
    public String toType(Class c)
    {
        return NAME;
    }

    /**
     * Validates that:
     *
     * <ol>
     *  <li>  the value is provided, if required as
     *        specified by map key <code>required</code>
     *  </li>
     *  <li>  the value is a number
     *  </li>
     *  <li>  the value is positive/negative/non-zero, if
     *        specified as such according to map keys
     *        <code>positive</code>, <code>negative</code>,
     *        and <code>nonZero</code>, respectively
     *  </li>
     *  <li>  the value does not exceed the precision
     *        specified by map key <code>precision</code>
     *  </li>
     *  <li>  the value does not exceed the scale specified
     *        by map key <code>scale</code>
     *  </li>
     *  <li>  the value does not contain the decimal
     *        separator, if the scale is 0
     *  </li>
     * </ol>
     *
     * @param  cfg  a map containing all the configuration
     *              information relevant to the local
     *              representation of this datatype
     *
     * @return      the string error message, if the
     *              input is invalid; otherwise, null
     *
     */
    public String validate(Map cfg)
    {
        // call super.validate()...
        String msg = super.validate(cfg);

        Object val = cfg.get(VALUE);
        if (msg == null && val != null)
        {
            BigDecimal num = getBigDecimal(cfg, VALUE);
            Boolean positive = getBoolean(cfg, POSITIVE);
            Boolean negative = getBoolean(cfg, NEGATIVE);
            Boolean nonZero = getBoolean(cfg, NON_ZERO);
            Integer precision = getInteger(cfg, PRECISION);
            Integer scale = getInteger(cfg, SCALE);
            BigDecimal maxValue = getBigDecimal(cfg, MAX_VALUE);
            BigDecimal minValue = getBigDecimal(cfg, MIN_VALUE);

            // first make sure that the input represents
            // a number
            if (num == null)
            {
                msg = ERROR_NOT_A_NUMBER;
            }

            // if the field is supposed to only accept
            // positive, negative, or non-zero values...
            // make sure the number value is appropriate
            else if (positive != null &&
                     positive.equals(Boolean.TRUE) &&
                     num.compareTo(ZERO) < 0)
            {
                msg = ERROR_NEGATIVE_NOT_PERMITTED;
            }
            else if (negative != null &&
                     negative.equals(Boolean.TRUE) &&
                     num.compareTo(ZERO) > 0)
            {
                msg = ERROR_POSITIVE_NOT_PERMITTED;
            }
            else if (nonZero != null &&
                     nonZero.equals(Boolean.TRUE) &&
                     num.compareTo(ZERO) == 0)
            {
                msg = ERROR_ZERO_NOT_PERMITTED;
            }

            // if the field has a maximum and/or minimum
            //value
            else if (minValue != null &&
                     num.compareTo(minValue) < 0)
            {
                msg = ERROR_EXCEEDS_MINIMUM_VALUE;
            }
            else if (maxValue != null &&
                     num.compareTo(maxValue) > 0)
            {
                msg = ERROR_EXCEEDS_MAXIMUM_VALUE;
            }

            // we have a number, now just make sure
            // it matches the precision & scale requested
            else if (precision != null)
            {
                int scaleValue = 0;
                if (scale != null)
                {
                    scaleValue = scale.intValue();
                }

                int decimal = val.toString().indexOf(DECIMAL_SEPARATOR);
                double max = Math.pow(10, precision.intValue());
                BigInteger precisionMax= new BigDecimal(max).unscaledValue();
                BigInteger unscaledValue = num.unscaledValue();

                // check against scale
                if (num.scale() > scaleValue)
                {
                    msg = ERROR_EXCEEDS_SCALE;
                }

                // check against precision
                else if (precisionMax.compareTo(unscaledValue) > 0)
                {
                    msg = ERROR_EXCEEDS_PRECISION;
                }

                // make sure we don't have a decimal point
                // in a non-decimal number
                else if (scale.intValue() == 0 && decimal > -1)
                {
                    msg = this.ERROR_DECIMAL_SEPARATOR_NOT_PERMITTED;
                }
            }
        }

        return msg;
    }
}